#!/usr/bin/env python
# We're using optparse because we need to support 2.6
# which doesn't have argparse.  Given that argparse is
# a dependency that eventually gets installed, we could
# try to bootstrap, but using optparse is just easier.
from __future__ import print_function

import optparse
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import tempfile

from contextlib import contextmanager

PACKAGES_DIR = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), 'packages')
INSTALL_DIR = os.path.expanduser(os.path.join(
    '~', '.local', 'lib', 'aws'))
GTE_PY37 = sys.version_info[:2] >= (3, 7)
UNSUPPORTED_PYTHON = (
    (2,6),
    (2,7),
    (3,3),
    (3,4),
    (3,5),
)
INSTALL_ARGS = (
    '--no-binary :all: --no-build-isolation --no-cache-dir --no-index '
)


class BadRCError(Exception):
    pass


class MultipleBundlesError(Exception):
    pass


class PythonDeprecationWarning(Warning):
    """
    Python version being used is scheduled to become unsupported
    in an future release. See warning for specifics.
    """
    pass


def _build_deprecations():
    # Example Template

    # py_27_params = {
    #    'date': 'July 15, 2021',
    #    'blog_link': 'https://aws.amazon.com/blogs/developer/announcing-end-'
    #                 'of-support-for-python-2-7-in-aws-sdk-for-python-and-'
    #                 'aws-cli-v1/'
    # }

    return {
        # (2,7): py_27_params
    }


DEPRECATED_PYTHON = _build_deprecations()


@contextmanager
def cd(dirname):
    original = os.getcwd()
    os.chdir(dirname)
    try:
        yield
    finally:
        os.chdir(original)


def run(cmd):
    sys.stdout.write("Running cmd: %s\n" % cmd)
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    stdout, stderr = p.communicate()
    if p.returncode != 0:
        output = (stdout + stderr).decode("utf-8")
        raise BadRCError("Bad rc (%s) for cmd '%s': %s" % (
            p.returncode, cmd, output))
    return stdout


def bin_path():
    """
    Get the system's binary path, either `bin` on reasonable
    systems or `Scripts` on Windows.
    """
    path = 'bin'

    if platform.system() == 'Windows':
        path = 'Scripts'

    return path


def create_install_structure(working_dir, install_dir):
    if not os.path.isdir(install_dir):
        os.makedirs(install_dir)
    create_virtualenv(location=install_dir, working_dir=working_dir)


def _create_virtualenv_internal(location, working_dir):
    # On py3 we use the built in venv to create our virtualenv.
    # There's a bug with sys.executable on external virtualenv
    # that causes installation failures.
    run('%s -m venv %s' % (sys.executable, location))


def _create_virtualenv_external(location, working_dir):
    # working_dir is used (generally somewhere in /tmp) so that we
    # don't modify the install/packages directories.
    with cd(PACKAGES_DIR):
        venv = _get_venv_package_tarball('.')
        compressed = tarfile.open(venv)
        compressed.extractall(path=working_dir)
        compressed.close()
    with cd(working_dir):
        # We know that virtualenv is the only dir in this directory
        # so we can listdir()[0] it.
        with cd(os.listdir('.')[0]):
            run(('%s virtualenv.py --no-download '
                 '--python %s %s') % (sys.executable,
                                      sys.executable,
                                      location))


def _get_package_tarball(package_dir, package_prefix):
    package_filenames = sorted([p for p in os.listdir(package_dir)
                                if p.startswith(package_prefix)])
    return package_filenames[-1]


def _get_venv_package_tarball(package_dir):
    return _get_package_tarball(package_dir, 'virtualenv')


def create_working_dir():
    d = tempfile.mkdtemp()
    return d


def pip_install_packages(install_dir):
    cli_tarball = [p for p in os.listdir(PACKAGES_DIR)
                   if p.startswith('awscli')]
    if len(cli_tarball) != 1:
        message = (
            "Multiple versions of the CLI were found in %s. Please clear "
            "out this directory before proceeding."
        )
        raise MultipleBundlesError(message % PACKAGES_DIR)
    cli_tarball = cli_tarball[0]
    pip_script = os.path.join(install_dir, bin_path(), 'pip')

    setup_requires_dir = os.path.join(PACKAGES_DIR, 'setup')
    with cd(setup_requires_dir):
        _install_setup_deps(pip_script, '.')

    with cd(PACKAGES_DIR):
        run('%s install %s --find-links file://%s %s' % (
                pip_script, INSTALL_ARGS, PACKAGES_DIR, cli_tarball))


def _install_setup_deps(pip_script, setup_package_dir):
    # Some packages declare `setup_requires`, which is a list of dependencies
    # to be used at setup time. These need to be installed before anything
    # else, and pip doesn't manage them.  We have to manage this ourselves
    # so for now we're explicitly installing the one setup_requires package
    # we need.  This comes from python-dateutils.
    setuptools_scm_tarball = _get_package_tarball(
        setup_package_dir, 'setuptools_scm')
    run('%s install --no-binary :all: --no-cache-dir --no-index '
        '--find-links file://%s %s' % (
            pip_script, setup_package_dir, setuptools_scm_tarball))
    wheel_tarball = _get_package_tarball(
        setup_package_dir, 'wheel')
    run('%s install --no-binary :all: --no-cache-dir --no-index '
        '--find-links file://%s %s' % (
            pip_script, setup_package_dir, wheel_tarball))


def create_symlink(real_location, symlink_name):
    if os.path.isfile(symlink_name):
        print("Symlink already exists: %s" % symlink_name)
        print("Removing symlink.")
        os.remove(symlink_name)
    symlink_dir_name = os.path.dirname(symlink_name)
    if not os.path.isdir(symlink_dir_name):
        os.makedirs(symlink_dir_name)
    os.symlink(real_location, symlink_name)
    return True


def main():
    parser = optparse.OptionParser()
    parser.add_option('-i', '--install-dir', help="The location to install "
                      "the AWS CLI.  The default value is ~/.local/lib/aws",
                      default=INSTALL_DIR)
    parser.add_option('-b', '--bin-location', help="If this argument is "
                      "provided, then a symlink will be created at this "
                      "location that points to the aws executable. "
                      "This argument is useful if you want to put the aws "
                      "executable somewhere already on your path, e.g. "
                      "-b /usr/local/bin/aws.  This is an optional argument. "
                      "If you do not provide this argument you will have to "
                      "add INSTALL_DIR/bin to your PATH.")
    py_version = sys.version_info[:2]
    if py_version in UNSUPPORTED_PYTHON:
        unsupported_python_msg = (
            "Unsupported Python version detected: Python {}.{}\n"
            "To continue using this installer you must use Python 3.6 "
            "or later.\n"
            "For more information see the following blog post: "
            "https://aws.amazon.com/blogs/developer/announcing-end-"
            "of-support-for-python-2-7-in-aws-sdk-for-python-and-"
            "aws-cli-v1/\n"
        ).format(py_version[0], py_version[1])
        print(unsupported_python_msg, file=sys.stderr)
        sys.exit(1)

    if py_version in DEPRECATED_PYTHON:
        params = DEPRECATED_PYTHON[py_version]
        deprecated_python_msg = (
            "Deprecated Python version detected: Python {}.{}\n"
            "Starting {}, the AWS CLI will no longer support "
            "this version of Python. To continue receiving service updates, "
            "bug fixes, and security updates please upgrade to Python 3.6 or "
            "later. More information can be found here: {}"
        ).format(
            py_version[0], py_version[1], params['date'], params['blog_link']
        )
        print(deprecated_python_msg, file=sys.stderr)

    opts = parser.parse_args()[0]
    working_dir = create_working_dir()
    try:
        create_install_structure(working_dir, opts.install_dir)
        pip_install_packages(opts.install_dir)
        real_location = os.path.join(opts.install_dir, bin_path(), 'aws')
        if opts.bin_location and create_symlink(real_location,
                                                opts.bin_location):
            print("You can now run: %s --version" % opts.bin_location)
        else:
            print("You can now run: %s --version" % real_location)
        print('\nNote: AWS CLI version 2, the latest major version '
              'of the AWS CLI, is now stable and recommended for general '
              'use. For more information, see the AWS CLI version 2 '
              'installation instructions at: https://docs.aws.amazon.com/cli/'
              'latest/userguide/install-cliv2.html')
    finally:
        shutil.rmtree(working_dir)


if GTE_PY37:
    create_virtualenv = _create_virtualenv_internal
else:
    create_virtualenv = _create_virtualenv_external


if __name__ == '__main__':
    main()
